package org.apache.sad.core.framework.jaxrpc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;

import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.Stub;
import javax.xml.rpc.handler.HandlerInfo;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.util.Assert;

import weblogic.wsee.security.unt.ClientUNTCredentialProvider;
import weblogic.xml.crypto.wss.WSSecurityContext;

import org.apache.sad.core.framework.configuration.ConfigurationManager;
import org.apache.sad.core.framework.configuration.PropertiesReloadedAware;
import org.apache.sad.core.framework.constants.ServiceConstants;
import org.apache.sad.core.framework.exception.ConnectionTimeoutException;
import org.apache.sad.core.framework.exception.ReadTimeoutException;
import org.apache.sad.core.framework.exception.SystemException;
import org.apache.sad.core.framework.jaxrpc.handler.WsseClientHandler;
import org.apache.sad.core.framework.jaxrpc.Endpoint;
import org.apache.sad.core.framework.jaxrpc.handler.ServiceContextHandler;
import org.apache.sad.core.framework.jaxrpc.JaxRpcRuntime;

/**
 * This class is used to create proxy and invoke the web services in static
 * mode. It hides the complexity of consuming web services. It handles
 * security, exception, endpoint or SOAP headers.
 *
 * The following properties are required.
 * <ul>
 * <li>serviceClass</li>
 * <li>endpoint</li>
 * <li>portName</li>
 * <li>targetNamespace</li>
 * </ul>
 * 
 *
 * The following properties are optional.
 * <ul>
 * <li>handlers</li>
 * <li>username</li>
 * <li>password</li>
 * </ul>
 *
 * To register handlers, add following property to spring configuration:
 *
 * <code>
 *			&lt;property name="handlers"&gt;
 *				&lt;list&gt; *
 *					&lt;value&gt;org.apache.sad.core.framework.jaxrpc.DebugHandler&lt;/value&gt;
 *				&lt;/list&gt;
 *			&lt;/property&gt;
 * </code>
 *
 * Enhanced to support properties reload at runtime.
 * 
 */
public class JaxRpcPortFactoryBean implements MethodInterceptor, InitializingBean, FactoryBean, BeanFactoryAware, ServiceConstants, PropertiesReloadedAware  {

    private static Log log = LogFactory.getLog(JaxRpcPortFactoryBean.class);
    private Object serviceProxy;
    private Class<?> serviceInterface;

    //required properties
    private JaxRpcRuntime runtime;
	private String portName;
	private String targetNamespace;
	private volatile String endpoint;

    //optional properties
	private Class<?>[] handlers;
	
	private Endpoint endpointConfig;
	
	private boolean wsse = true;
	private boolean addNonce = false;
	private volatile String username;
	private volatile String password;
	private boolean usePasswordDigest = false;
	private boolean addTimestamp = false;
	private String domainName;
	private String serviceName;
	private String version;

	private String requestHeaderNamespace;
	
	private boolean propagateIdentity = false;
	
	private ConfigurationManager timeoutConfigurationManager;
	
	/**
	 * Specifies, in milliseconds, how long a client application that is
	 * attempting to invoke a Web Service waits to make a
	 * connection. After the specified time elapses, if a
	 * connection hasnt been made, the attempt times out.
	 */
	private Integer connectionTimeoutInMilliSeconds = 1000;
	
	/**
	 * Specifies, in milliseconds, how long a client application waits
     * for a response from a Web Service it is invoking. After the
     * specified time elapses, if a response hasn�t arrived, the
     * client times out.
	 */
	private Integer readTimeoutInMilliSeconds = 15000;

	private String sourceSystemId;
	private String sourceServerId;
	private String userId;
	private Integer connectRetry = 1;
	private Integer sleepTimeInMilliSeconds = 1000;

	private static final String CONNECT_UNKNOWNHOST = "UnknownHostException";
	private static final String CONNECT_TIMEDOUT = "connect timed out";


	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getSourceServerId() {
		return sourceServerId;
	}

	public void setSourceServerId(String sourceServerId) {
		this.sourceServerId = sourceServerId;
	}

	public String getSourceSystemId() {
		return sourceSystemId;
	}

	public void setSourceSystemId(String sourceSystemId) {
		this.sourceSystemId = sourceSystemId;
	}

	public Integer getConnectRetry() {
		return connectRetry;
	}

	public void setConnectRetry(Integer connectRetry) {
		this.connectRetry = connectRetry;
	}

	public Integer getSleepTimeInMilliSeconds() {
		return sleepTimeInMilliSeconds;
	}

	public void setSleepTimeInMilliSeconds(Integer sleepTimeInMilliSeconds) {
		this.sleepTimeInMilliSeconds = sleepTimeInMilliSeconds;
	}
	
	private BeanFactory beanFactory;	

	public Object getObject() throws Exception {
        return serviceProxy;
    }

    public Class<?> getObjectType() {
    	return serviceProxy == null ? serviceInterface : serviceProxy.getClass();
    }

    public boolean isSingleton() {
        return true;
    }

	private boolean isRetry(final String methodName, final String messageCode, final String message, final int retryCounter) {
		if (((messageCode != null) && (messageCode.contains(CONNECT_UNKNOWNHOST)))  || ((message != null) && (message.contains(CONNECT_TIMEDOUT)))) {
        	log.info(CONNECT_UNKNOWNHOST + " or " + CONNECT_TIMEDOUT + " occurred in " + methodName + " connectRetry " + connectRetry + " sleepTimeInMilliSeconds " + sleepTimeInMilliSeconds);
			if (retryCounter < connectRetry) {
				try {
					Thread.sleep(sleepTimeInMilliSeconds);
				} catch (InterruptedException ie) {
					log.error("InterruptedException has occurred. Message is " + ie.getMessage(), ie);
				}
				return true;
			}
		}
		return false;
	}
	
	@SuppressWarnings("unchecked")
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Service jaxRpcService = runtime.createJaxRpcService();
		Stub stub = runtime.getPort(jaxRpcService, portName);

		if (AopUtils.isToStringMethod(invocation.getMethod())) {
			return this.toString();
		}

		// Gets a local copy of endpointConfig so that we won't be impacted
		// by properties reloading mechanism which could leave configurations
		// (endpoint, username and password) in an inconsistent state for 
		// the request.
		Endpoint localEndpointConfig =  this.endpointConfig;

		// Checks whether the interface supports dynamic endpoint. If yes, use
		// the endpoint parameter passed in the method call. Also get the actual
		// method and arguments needed to call the stub.
		Method method = invocation.getMethod();
		Object[] arguments = invocation.getArguments();
		Class<?>[] parameterTypes = method.getParameterTypes();
		if  (parameterTypes.length > 0 && Endpoint.class.equals(parameterTypes[parameterTypes.length-1])) {
			localEndpointConfig = (Endpoint)arguments[arguments.length-1];
			if ( localEndpointConfig == null ) {
				throw new NullPointerException("Endpoint can not be null.");
			}
			method = stub.getClass().getMethod(method.getName(), (Class[])ArrayUtils.subarray(parameterTypes, 0, parameterTypes.length - 1));
			arguments = ArrayUtils.subarray(arguments, 0, arguments.length - 1);
			if ( log.isDebugEnabled() ) {
				log.debug(String.format("Using dynamic endpoint: %s for service '%s'", localEndpointConfig, serviceInterface.getName()));
			}
		}

		if ( connectionTimeoutInMilliSeconds != null ) {
			runtime.setConnectionTimeout(stub, connectionTimeoutInMilliSeconds);
		}

		if ( readTimeoutInMilliSeconds != null ) {
			runtime.setReadTimeout(stub, getReadTimeoutInMilliSeconds(invocation));
		}

		//String endpointAddress = getConsumableEndpoint();
		//if (StringUtils.isEmpty(endpointAddress)) {
		String endpointAddress = localEndpointConfig.getEndpoint();
		//}

		if ( log.isDebugEnabled() ) {
			log.debug("Endpoint address: " + endpointAddress);
		}

		stub._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, endpointAddress);
		QName pn = new QName(targetNamespace, portName);
		HashMap<String, Object> hMap;

		String username = localEndpointConfig.getUsername();
		String password = localEndpointConfig.getPassword();

		org.apache.sad.core.framework.jaxrpc.ServiceContext serviceContext = ServiceContextHandler.getInstance().getServiceContext();

		if(sourceServerId != null)
			serviceContext.setSourceServerId(sourceServerId);
		if(sourceSystemId!= null)
			serviceContext.setSourceSystemId(sourceSystemId);
		if(userId != null)
			serviceContext.setUserId(userId);

		if ( isPropagateIdentity()) {
			if (ServiceContextHandler.getInstance().getServiceContext().getWsSecurityUser() != null){
				username = ServiceContextHandler.getInstance().getServiceContext().getWsSecurityUser();
			} 
			if(ServiceContextHandler.getInstance().getServiceContext().getWsSecurityCredential() != null){
				password = ServiceContextHandler.getInstance().getServiceContext().getWsSecurityCredential();
			}

		}

		//Detects if WsseClientHandler is configured, if yes use it to
		//insert wsse headers.
		boolean isWsseClientHandlerConfigured = false;
		//Register handlers.
		if ( handlers != null ) {
			for (Class<?> handler : handlers ) {
				if ( WsseClientHandler.class.isAssignableFrom(handler) ) {
					isWsseClientHandlerConfigured = true;
				}
				hMap = new HashMap<String, Object>();
				if (wsse) {
					hMap.put("username", username);
					hMap.put("password", password);
					hMap.put("wsse",Boolean.valueOf(this.isWsse()));
					hMap.put("addNonce", Boolean.valueOf(this.getAddNonce()));
					hMap.put("usePasswordDigest", Boolean.valueOf(this.getUsePasswordDigest()));
					hMap.put("addTimestamp", Boolean.valueOf(this.getAddTimestamp()));
					hMap.put("endpoint", localEndpointConfig.getEndpoint());
				}
				hMap.put(REQUEST_HEADER_NS_KEY, this.getRequestHeaderNamespace());
				jaxRpcService.getHandlerRegistry().getHandlerChain(pn).add(new HandlerInfo(handler, hMap, null));
			}
		}

		if ( username != null && !isWsseClientHandlerConfigured ) {
			if ( wsse ) {
				List<ClientUNTCredentialProvider> providers = new ArrayList<ClientUNTCredentialProvider>();
				providers.add(new ClientUNTCredentialProvider(username.getBytes(), password.getBytes()));
				stub._setProperty(WSSecurityContext.CREDENTIAL_PROVIDER_LIST, providers);        		
			} else if ( !wsse ) {
				stub._setProperty(Stub.USERNAME_PROPERTY, username);
				stub._setProperty(Stub.PASSWORD_PROPERTY, password);
			}
		}

		String apiName = invocation.getMethod().getName();
		String backEndName =((Advised)this.serviceProxy).getProxiedInterfaces()[0].getName();


		//>@> Added RETRY logic
		boolean isRetry = false;
		int retryCounter = 1;

		do {
			isRetry = false;

			try {        	
				preInvoke();
				//Assume stub object implements Remote
				return invoke(method, arguments,(Remote)stub);
			}
			
			catch (Throwable t) {
	        	if (t instanceof SystemException) {
            		final String methodName = getClass().getName()+ " " + method.getName();
	        		SystemException sysExp = (SystemException) t;
	        		if ((sysExp.getMessages() != null) && (sysExp.getMessages().length > 0)) {
		            	log.debug("SystemException occurred in " + methodName + " Code:" + sysExp.getMessages()[0].getCode() + " Message:" + sysExp.getMessages()[0].getDefaultMessage());
		            	final String messageCode = sysExp.getMessages()[0].getCode();
		            	final String message = sysExp.getMessages()[0].getDefaultMessage();
	                	isRetry = isRetry(methodName, messageCode, message, retryCounter);
						if (isRetry) {
							++retryCounter;
							log.debug(methodName + " will be retried again. The retry counter is " + retryCounter);
						} else {
							log.debug("Exhausted all retry for " + methodName + ". Throwing Exception to the caller.");
						}
	        		}
	        	}

	        	if (!isRetry) {
	        		log.debug("Invoke Service for "+retryCounter+ " times(s), No More Retries.");
		        	throw t;
	        	}
			} finally {
				postInvoke();
			}
		} while(isRetry);
		return null;
	}

	private Integer getReadTimeoutInMilliSeconds(MethodInvocation invocation) {
    	if ( timeoutConfigurationManager != null ) {
    		String timeoutPropKey = invocation.getMethod().getDeclaringClass().getSimpleName() + "." + invocation.getMethod().getName() + "." + "readTimeoutInMilliSeconds";
    		try {
    			int timeoutInMilliSeconds = timeoutConfigurationManager.getInt(timeoutPropKey);
    			if (log.isDebugEnabled() ) {
    				log.debug("Found timeout setting:" + timeoutPropKey + "=" + timeoutInMilliSeconds);
    			}
    			if ( timeoutInMilliSeconds > 0 ) {
    				return timeoutInMilliSeconds;
    			}
    		} catch (NoSuchElementException nsee) {
    			if (log.isDebugEnabled() ) {
    				log.debug("Could not find timeout setting for " + timeoutPropKey + ", use default timeout setting:" + readTimeoutInMilliSeconds);
    			}
    		}
    	}
    	return readTimeoutInMilliSeconds;
	}

	/**
     * Does the extra initialization after the properties have been set.
     *
     * @throws exception
     */
    public void afterPropertiesSet() throws Exception {
    	Assert.notNull(runtime, "JaxRpcRuntime must be specified.");
    	Assert.notNull(targetNamespace, "targetNamespace must be specified.");
    	Assert.notNull(portName, "portName must be specified.");
    	Assert.notNull(endpoint, "endpoint must be specified.");
    	Assert.notNull(serviceInterface, "serviceInterface must be specified.");
        
        if ( timeoutConfigurationManager == null ) {
        	//Get the default configuration manager
        	try {
        		timeoutConfigurationManager = (ConfigurationManager)beanFactory.getBean(ConfigurationManager.DEFAULT_CONFIGURATION_MANGER_BEAN_NAME);
        	} catch (NoSuchBeanDefinitionException nsbde) {
        		if ( log.isDebugEnabled() ) {
        			log.debug("No configuration manager is set for contract-by-contract basis timeout settings for " + getServiceInterface() + ", will use default timeout setting:" + readTimeoutInMilliSeconds + " for all contracts.");
        		}
        	}
        }

        this.endpointConfig = new Endpoint(this.endpoint, this.username, this.password);
        
        log.info("DomainName = " + this.getDomainName() + ", ServiceName = " + this.getServiceName() + ", Version = " + this.getVersion());
        serviceProxy = ProxyFactory.getProxy(serviceInterface, this);
    }


	public void propertiesReloaded(Properties updatedProperties) {
		this.endpointConfig = new Endpoint(this.endpoint, this.username, this.password);
	}

    /**
     *
     * Performs extra preperation before invoking the web service.
     *
     */
    protected void preInvoke(){
    }

    /**
     *
     * Performs extra clean up after invoking the web service.
     *
     */
    protected void postInvoke(){
    }

    protected Object invoke(Method method, Object[] args, Remote stub) throws Throwable {    	
        try {        	
            if (method.getDeclaringClass().isInstance(stub)) {
                // directly implemented
                return method.invoke(stub, args);
            } else {
                // not directly implemented
                String methodName = method.getName();
                methodName = methodName.substring(0,1).toLowerCase() + methodName.substring(1);                
                Method stubMethod = stub.getClass().getMethod(methodName, method.getParameterTypes());
                return stubMethod.invoke(stub, args);
            }
        } catch (InvocationTargetException e) {
            Throwable targetEx = e.getTargetException();
            if (targetEx instanceof RemoteException) {
                RemoteException rex = (RemoteException) targetEx;
                log.warn("RemoteException occurred: ", rex);
                throw runtime.handleRemoteException(rex);
            } else {
            	if (targetEx instanceof Exception) {
                	if (targetEx instanceof RuntimeException) {
                        throw new SystemException("Failed to invoke remote web service.", targetEx);            		
//                	} else if (targetEx.getClass().getSimpleName().equals("ExceptionType")) {
//                		throw BaseException.fromExceptionType((Exception)targetEx);
                	} else {
                		throw targetEx;
                	}          		
            	} else {
            		//error                    
                    throw new SystemException("Failed to invoke remote web service.", targetEx);            		
            	}            	
            }
        } catch (Exception e) {            
            throw new SystemException("Failed to invoke remote web service.", e);
        } 
    }

    public void setServiceInterface(Class<?> serviceInterface) {
        if ((serviceInterface != null) && !serviceInterface.isInterface()) {
            throw new SystemException("Invalid serviceInterface: " + serviceInterface);
        }
        this.serviceInterface = serviceInterface;
    }
  
    public void setJaxRpcRuntime(JaxRpcRuntime value) {
        this.runtime = value;
    }

	public void setEndpoint(String endpoint) {
		this.endpoint = endpoint;
	}

	public Class<?>[] getHandlers() {
		return handlers;
	}

	public void setHandlers(Class<?>[] handlers) {
		this.handlers = handlers;
	}

	public String getPortName() {
		return portName;
	}

	public void setPortName(String portName) {
		this.portName = portName;
	}

	public String getTargetNamespace() {
		return targetNamespace;
	}

	public void setTargetNamespace(String targetNamespace) {
		this.targetNamespace = targetNamespace;
	}
	
	public JaxRpcRuntime getJaxRpcRuntime() {
		return runtime;
	}

	public Class<?> getServiceInterface() {
		return serviceInterface;
	}
	
	public Endpoint getCopyOfEndpointConfig() {
		return endpointConfig.copy();
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public boolean getUsePasswordDigest() {
		return this.usePasswordDigest;
	}
	
	public void setUsePasswordDigest(boolean usePasswordDigest) {
		this.usePasswordDigest = usePasswordDigest;
	}
		
	public boolean getAddTimestamp() {
		return addTimestamp;
	}

	public void setAddTimestamp(boolean addTimestamp) {
		this.addTimestamp = addTimestamp;
	}

	public boolean getAddNonce() {
		return addNonce;
	}

	public void setAddNonce(boolean addNonce) {
		this.addNonce = addNonce;
	}
	
	public boolean isWsse() {
		return wsse;
	}

	public void setWsse(boolean wsse) {
		this.wsse = wsse;
	}

	public Integer getConnectionTimeoutInMilliSeconds() {
		return connectionTimeoutInMilliSeconds;
	}

	public void setConnectionTimeoutInMilliSeconds(Integer connectionTimeout) {
		this.connectionTimeoutInMilliSeconds = connectionTimeout;
	}

	public Integer getReadTimeoutInMilliSeconds() {
		return readTimeoutInMilliSeconds;
	}

	public void setReadTimeoutInMilliSeconds(Integer readTimeout) {
		this.readTimeoutInMilliSeconds = readTimeout;
	}

	public ConfigurationManager getTimeoutConfigurationManager() {
		return timeoutConfigurationManager;
	}

	public void setTimeoutConfigurationManager(
			ConfigurationManager timeoutConfigurationManager) {
		this.timeoutConfigurationManager = timeoutConfigurationManager;
	}

	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = beanFactory;
	}

	public String getDomainName() {
		return domainName;
	}

	public void setDomainName(String domainName) {
		this.domainName = domainName;
	}

	public String getServiceName() {
		return serviceName;
	}

	public void setServiceName(String serviceName) {
		this.serviceName = serviceName;
	}

	public String getVersion() {
		return version;
	}

	public void setVersion(String version) {
		this.version = version;
	}

	public String getRequestHeaderNamespace() {
		return requestHeaderNamespace;
	}

	public void setRequestHeaderNamespace(String requestHeaderNamespace) {
		this.requestHeaderNamespace = requestHeaderNamespace;
	}
	
	
	public boolean isPropagateIdentity() {
		return propagateIdentity;
	}

	public void setPropagateIdentity(boolean propagateIdentity) {
		this.propagateIdentity = propagateIdentity;
	}
}
